const path = require("path");
const fs = require("fs");
const parser = require("@babel/parser");
const types = require("@babel/types");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

function normalizePath(path) {
  return path.replace(/\\/g, "/");
}

const baseDir = normalizePath(process.cwd());

module.exports = class Compilation {
  constructor(options, compiler) {
    this.options = options;
    this.compiler = compiler;
    this.modules = []; // 本次编译的所有模块
    this.chunks = []; // 本次编译的代码块
    this.assets = {}; // key是文件名,值是文件内容
    this.files = []; // 本次打包出来的文件
    this.fileDependencies = new Set(); // 本次编译依赖的文件或者说模块
  }
  build(callback) {
    let entry = {};
    if (typeof this.options.entry === "string") {
      entry.main = this.options.entry;
    } else {
      entry = this.options.entry;
    }
    for (let [entryName, entryPath] of Object.entries(entry)) {
      const entryFilePath = path.posix.join(baseDir, entryPath);
      this.fileDependencies.add(entryFilePath);
      // 从入口文件出发, 调用所有的loader对文件进行编译
      const entryModule = this.buildModule(entryName, entryFilePath);
      this.modules.push(entryModule);
      // 根据入口和模块之间的依赖关系，组装成一个个包含多个模块的 Chunk
      const chunk = {
        name: entryName,
        entryModule,
        modules: this.modules.filter((m) =>m.names.includes(entryName)),
      };
      this.chunks.push(chunk);
    }
    console.log(this.chunks, 333)
    // 再把每个 Chunk 转换成一个单独的文件加入到输出列表
    this.chunks.forEach((chunk) => {
      const filename = this.options.output.filename.replace(
        "[name]",
        chunk.name
      );
      this.files.push(filename);
      this.assets[filename] = getSource(chunk);
    });
    callback(null, {
      modules: this.modules,
      chunks: this.chunks,
      assets: this.assets,
      files: this.files,
    }, this.fileDependencies)
  }
  buildModule(moduleName, modulePath) {
    console.log(`开始编译Name: ${moduleName}`);
    console.log(`开始编译Path: ${modulePath}`);
    // 1. 读取文件的内容
    let sourceCode = fs.readFileSync(modulePath, "utf8");
    // 2. 根据匹配规则找到所有loader对文件进行解析
    const rules = this.options.module.rules;
    const loaders = [];
    rules.forEach((rule) => {
      if (modulePath.match(rule.test)) {
        loaders.push(...rule.use);
      }
    });
    sourceCode = loaders.reduceRight((code, loaderPath) => {
      return require(loaderPath)(code);
    }, sourceCode);
    // 3. 找出该模块依赖的模块, 再递归继续解析, 直到所有的依赖文件都处理过
    // 3.1 声明当前模块的id
    const moduleId = "./" + path.posix.relative(baseDir, modulePath);
    const module = { id: moduleId, dependencies: [], names: [moduleName] };
    const ast = parser.parse(sourceCode, { sourceType: "module" });
    traverse(ast, {
      CallExpression: ({ node }) => {
        if (node.callee.name === "require") {
          let depModuleName = node.arguments[0].value,
            depModulePath;
          if (depModuleName.startsWith(".")) {
            const currentDir = path.posix.dirname(modulePath);
            // 引入模块的相对路径
            depModulePath = path.posix.join(currentDir, depModuleName);
            // 根据配置文件补充后缀
            const extensions = this.options.resolve.extensions;
            depModulePath = tryExtensions(extensions, depModulePath);
          } else {
            // 如果不是以.开头，则从node_modules中加载
            depModulePath = require.resolve(depModuleName);
          }
          this.fileDependencies.add(depModulePath);
          const depModuleId =
            "./" + path.posix.relative(baseDir, depModulePath);
          // 修改语法树? => types.stringLiteral(depModuleId) './scr/title.js'
          node.arguments[0] = types.stringLiteral(depModuleId);
          // 把依赖项放置到当前模块的数组中
          module.dependencies.push({
            depModuleId,
            depModulePath,
          });
        }
      },
    });
    //使用改造后的ast语法要地重新生成新的源代码
    const { code } = generator(ast);
    module._source = code;
    // 依次打包该模块的依赖项
    module.dependencies.forEach(({ depModuleId, depModulePath }) => {
      // 判断次模块是否已经打包过了
      const existModule = this.modules.find((m) => m.id === depModuleId);
      if (existModule) {
        existModule.names.push(moduleName);
      } else {
        const depModule = this.buildModule(moduleName, depModulePath);
        this.modules.push(depModule);
      }
    });
    return module;
  }
};

function tryExtensions(extensions, modulePath) {
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (let i = 0; i < extensions.length; i++) {
    const extension = extensions[i];
    const filePath = modulePath + extension;
    if (fs.existsSync(filePath)) {
      return filePath;
    }
  }
  throw new Error(`Cannot find module '${modulePath}'`);
}

function getSource(chunk) {
  return `
  (() => {
    var modules = {
      ${chunk.modules
        .map(
          (module) => `
          "${module.id}": module => {
            ${module._source}
          }
        `
        )
        .join(",")}
    };
    var cache = {};
    function require(moduleId) {
      var cachedModule = cache[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = cache[moduleId] = {
        exports: {}
      };
      modules[moduleId](module, module.exports, require);
      return module.exports;
    }
    var exports = {};
    (() => {
      ${chunk.entryModule._source}
    })();
  })();
  `;
}
